
package br.cin.stadium.middleware.network;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;

import br.cin.stadium.middleware.directory.ServerLocation;
import br.cin.stadium.middleware.event.Event;
import br.cin.stadium.middleware.marshall.Marshaller;
import br.cin.stadium.middleware.media.Media;
import br.cin.stadium.middleware.media.MediaReference;
import br.cin.stadium.middleware.network.config.NetConfig;
import br.cin.stadium.middleware.util.Logger;

/**
 * 
 * NetworkHandler is responsible to listen incoming connection and create
 * SocketConnection to handle them. All outgoing connection is also handled by
 * this class.
 * 
 * @author Fabiano, Jorge, Rafael and Vando.
 */
public class NetworkHandler implements Runnable {

	/**
	 * Server socket instance - Listen for new connection all the time.
	 */
	private ServerSocket serverSocket = null;

	/**
	 * Indicate that this network handle main thread is end (connections
	 * closed). Used to threads communications.
	 */
	private boolean exit = false;

	/**
	 * Client socket references - to close clients when desires to finalize
	 * middleware.
	 */
	private HashMap<Integer, SocketConnection> activedSocketList;

	/**
	 * Unique networkHandle instance.
	 */
	private static NetworkHandler instance;
	
	private String ip;

	/**
	 * Private Constructor - call by getInstance method.
	 */
	private NetworkHandler() {
		Logger.log("NetworkHandler", "Constructor", "Begin");
		this.exit = false;
		
		try {
			this.ip = InetAddress.getLocalHost().toString().split("/")[1];
		} catch (UnknownHostException e) {
			Logger.logError("Unnable to get the ip from the device.", e);
		}
		
		Logger.log("NetworkHandler", "Constructor", "END");
	}

	/**
	 * GetInstance uses Singleton pattern - all calls to this class will be made
	 * by this unique instance.
	 * 
	 * @return The unique NetowrkHandle instance.
	 */
	public static NetworkHandler getInstance() {
		if (instance == null) {
			instance = new NetworkHandler();
		}

		return instance;
	}

	/**
	 * Call to start to listen incoming connections.
	 * 
	 * @throws NetworkException
	 *             To inform if an error occurred.
	 */
	public void start() throws NetworkException {
		Logger.log("NetworkHandler", "start", "Begin");

		if (this.serverSocket != null) {
			return;
		}
		
		try {
			activedSocketList = new HashMap<Integer, SocketConnection>();
			serverSocket = new ServerSocket(NetConfig.STADIUM_PORT_CLIENT_LISTEN);
			
			new Thread(this).start();
			
		} catch (Exception e) {
			throw new NetworkException("Unable to start listenning NetworkHandler on Port " + NetConfig.STADIUM_PORT_CLIENT_LISTEN);
		}

		Logger.log("NetworkHandler", "start", "End");
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Runnable#run()
	 */
	public void run() {
		Logger.log("NetworkHandler", "run", "Begin");

		while (!exit) {
			try {
				Socket socketAccept = serverSocket.accept();
				Logger.log("", "", "New Connection !!!");
				
				SocketConnection sc = new SocketConnection(socketAccept);
				this.activedSocketList.put(sc.getSocketID(), sc);
				sc.start();

			} catch (IOException e) {
				Logger.logError("Error to create Socket connection", e);
			}
		}
		Logger.log("NetworkHandler", "run", "End");
	}

	public void closeConnection() {
		Logger.log("NetworkHandler", "closeConnection", "Begin");

		try {
			if (serverSocket != null) {
				this.serverSocket.close();
				this.serverSocket = null;
			}

		} catch (IOException e) {
			// empty
		} finally {
			exit = true;
			instance = null;
		}

		if (this.activedSocketList != null) {
			Iterator<SocketConnection> sockets = this.activedSocketList
					.values().iterator();
			while (sockets.hasNext()) {
				try {
					SocketConnection sc = sockets.next();
					sc.closeConnection();

				} catch (Exception e) {
					// TODO: handle exception
				}
			}
		}

		Logger.log("NetworkHandler", "closeConnection", "End");
	}

	/**
	 * Create a SocketConnection object to RECEIVE data from network.
	 * 
	 * @param ip
	 * @param port
	 * @return
	 */
	public static byte[] createConnection(String ip, int port) {
		Logger.log("NetworkHandler", "createConnection", "Begin (" + ip + ","
				+ port + ")");
		byte[] bytes = null;
		SocketConnection sc = null;

		try {
			Socket clientSocket = new Socket(ip, port);

			sc = new SocketConnection(clientSocket);
			bytes = sc.processFromApp();

		} catch (Exception e) {
			Logger.logError("Cannot create the socket.", e);
		} finally {
			sc.closeConnection();
		}

		Logger.log("NetworkHandler", "createConnection", "End (" + bytes.length
				+ ")");
		return bytes;

	}

	/**
	 * Create a SocketConnection object to SEND data from network.
	 * 
	 * @param params
	 * @throws NetworkException
	 */
	public void sendData(Object[] params, byte[] data) throws NetworkException {
		Logger.log("NetworkHandler", "sendData", "Begin(" + params.length
						+ ")");
		SocketConnection sc = null;
		int error = 0;

		for (int i = 0; i < params.length; i++) {
			try {
				ServerLocation sl = (ServerLocation) params[i];
				Logger.log("Sending data to param " + sl);

				ServerLocation myServer = new ServerLocation(this.getMyIp(),NetConfig.STADIUM_PORT_CLIENT_LISTEN);
				
				if(sl.equals(myServer)) {
					continue;
				}
				
				Socket clientSocket = new Socket(sl.getIp(), Integer.parseInt(sl.getPort()));
				sc = new SocketConnection(clientSocket);

				sc.sendDataFromApp(data);

			} catch (NetworkException ne) {
				Logger.logError("NetworkHandler", "sendData", "Error-ne", ne);
				error++;
			} catch (Exception e) {
				Logger.logError("NetworkHandler", "sendData", "Error-e", e);
				error++;

			} finally {
				if(sc != null) {
					sc.closeConnection();
				}
			}
		}// FOR

		if (error > 0) {
			throw new NetworkException(
					"Error - probably some data was not sent");
		}

		Logger.log("NetworkHandler", "sendData", "End(" + error + ")");
	}

	public Media requestMedia(MediaReference mediaReference) throws NetworkException {
		Logger.log("NetworkHandler", "requestMedia", "Begin ("
				+ mediaReference.getMediaName() + ","
				+ mediaReference.getSector() + ","
				+ mediaReference.getServerLocation().getIp() + ","
				+ mediaReference.getServerLocation().getPort() + ")");

		Media retorno = new Media();
		Socket socket = null;
		OutputStream os = null;
		InputStream is = null;
		DataOutputStream mdos = null;
		DataInputStream mdis = null;

		try {
			socket = new Socket(mediaReference.getServerLocation().getIp(),
					Integer.parseInt(mediaReference.getServerLocation()
							.getPort()));

			os = socket.getOutputStream();

			mdos = new DataOutputStream(new BufferStream(os));
			mdos.writeInt(Event.OPCODE_REQUEST_MEDIA);
			mdos.writeUTF(mediaReference.getMediaName());
			os.flush();

			is = socket.getInputStream();
			mdis = new DataInputStream(new BufferStream(is));
			int available = mdis.readInt();

			Logger.log("AVAILABLE = " + available);

			int lag = 0;
			int numread = 0;
			int numatual = 0;

			long BEGIN = System.currentTimeMillis();

			byte[] raw = new byte[available];
			while (lag < 8) {
				numread = is.available();
				if (numread <= 0) {
					lag++;
					try {
						Thread.sleep(100);
						continue;
					} catch (InterruptedException e) {
						// empty
					}
				}
				lag = 0;
				is.read(raw, numatual, numread);
				numatual += numread;
			}

			long END = System.currentTimeMillis();
			Logger.log("DIFF = " + (END - BEGIN));

			Marshaller.unmarshall(retorno, raw);
		} catch (IOException e) {
			String msg = "Unnable to open connection to (" + mediaReference.getServerLocation().getIp() + ","
	                      + mediaReference.getServerLocation().getPort() + ")";
			Logger.logError(msg, e);
			throw new NetworkException(msg);
		} finally {
			try {
				if (os != null) {
					os.close();
				}
				if (is != null) {
					is.close();
				}
				if (socket != null) {
					socket.close();
				}
			} catch (Exception e2) {
				// empty
			}
		}

		Logger.log("NetworkHandler", "requestMedia", "End (" + retorno + ")");
		return retorno;

	}
	
	// ====================================================
	
	public ArrayList<MediaReference> connectAndsearchForMedia(ServerLocation server, ArrayList<Object> params) throws NetworkException {
		Logger.log("MediaManager", "connectAndsearchForMedia", "Begin ((" + server.getIp() + "," + server.getPort() + ")," + params.size() + ")"); 
		
		ArrayList<MediaReference> retorno = new ArrayList<MediaReference>();
		MediaReference retornoClass = new MediaReference();
		
		Socket socket = null;
		OutputStream os = null;
		InputStream is = null;
		
		try {
			socket = new Socket(server.getIp(), Integer.parseInt((server.getPort())));
			os = socket.getOutputStream();
			
			DataOutputStream mdos = new DataOutputStream(new BufferStream(os));
			
			for (int i = 0; i < params.size(); i++) {
				
				Object obj = params.get(i);
				if(obj instanceof Integer) {
					mdos.writeInt(((Integer)obj).intValue());
					
				} else if(obj instanceof String) {
					mdos.writeUTF(obj.toString());
				}
			}

			os.flush();
			is = socket.getInputStream();
			
			DataInputStream mdis = new DataInputStream(new BufferStream(is));
			int available = mdis.readInt();
		    
		    Logger.log("AVAILABLE = " + available);

			byte[] raw = new byte[available];
			int counter = 0;			
			
			long beginTime = System.currentTimeMillis();
			while(counter < available) {
				raw[counter++] = (byte)is.read();
			}
			long endTime = System.currentTimeMillis();
			
			Logger.log("DIFF = " + (endTime - beginTime));
			
			Marshaller.unmarshall(retornoClass, retorno, raw);
			
		} catch (IOException e) {
			String msg = "Error (" + server.getIp() + "," + server.getPort() + ")";
			
			Logger.logError(msg, e);
			throw new NetworkException(msg);
			
		} finally {
			try {
				if (os != null) {
					os.close();
				}
				if (is != null) {
					is.close();
				}
				if (socket != null) {
					socket.close();
				}
			} catch (Exception e2) {
				// empty
			}

		}
		Logger.log("MediaManager", "connectAndsearchForMedia", "End (" + retorno.size() + ")"); 
		return retorno;
	}
	

	/**
	 * Get Device IP
	 * 
	 * @return Device IP
	 */
	public String getMyIp() {
		Logger.log("NetworkHandler", "getMyIp", "Begin");

		//this.ip = "localhost";
		this.ip = "10.0.2.2";
		
		Logger.log("NetworkHandler", "getMyIp", "End(" + ip + ")");
		return this.ip;
	}

}